UNPKG

14.6 kBJavaScriptView Raw
1import fs from 'fs-extra'
2import path from 'path'
3
4import require_hacker from 'require-hacker'
5import serialize from '../tools/serialize-javascript'
6
7import { exists, clone, replace_all, starts_with, last } from '../helpers'
8import { alias_hook, uniform_path } from '../common'
9
10// writes webpack-assets.json file, which contains assets' file paths
11export default function write_assets(json, options, log)
12{
13 // take the passed in options
14 options = clone(options)
15
16 log.debug(`running write assets webpack plugin v${require('../../package.json').version} with options`, options)
17
18 // make webpack stats accessible for asset functions (parser, path, filter)
19 options.webpack_stats = json
20
21 const development = options.development
22
23 if (development)
24 {
25 log.debug(' (development mode is on)')
26 }
27
28 // write webpack stats json for debugging purpose
29 if (options.debug)
30 {
31 // write webpack stats file
32 log.debug(`writing webpack stats to ${options.webpack_stats_path}`)
33
34 // write the file
35 // (format the JSON for better readability)
36 fs.outputFileSync(options.webpack_stats_path, JSON.stringify(json, null, 2))
37 }
38
39 // the output object with assets
40 const output = options.output
41
42 // populate the output object with assets
43 populate_assets(output, json, options, log)
44
45 // write webpack assets info file
46 if (options.output_to_a_file)
47 {
48 // format the JSON for better readability if in debug mode
49 const assets_info = development ? JSON.stringify(output, null, 2) : JSON.stringify(output)
50
51 // rewrite `webpack-assets.json`
52 let rewrite = true
53
54 // for `webpack-assets.json` caching to work
55 // chunks info should be moved out of it,
56 // otherwise chunk hashsums constantly change,
57 // and there won't be any caching.
58 //
59 // const assets_buffer = Buffer.from(assets_info)
60 //
61 // // if webpack-assets.json already exists,
62 // // then maybe no need to rewrite it
63 // if (fs.existsSync(options.webpack_assets_path))
64 // {
65 // // previously written webpack-assets.json
66 // const previous_assets_buffer = fs.readFileSync(options.webpack_assets_path)
67 //
68 // // if webpack-assets.json rewrite is not needed, then don't do it
69 // if (assets_buffer.equals(previous_assets_buffer))
70 // {
71 // rewrite = false
72 // }
73 // }
74
75 // if webpack-assets.json rewrite is needed, then do it
76 if (rewrite)
77 {
78 log.debug(`writing webpack assets info to ${options.webpack_assets_path}`)
79 // write the file
80 fs.outputFileSync(options.webpack_assets_path, assets_info)
81 }
82 }
83 else
84 {
85 log.debug(`serving webpack assets from memory`)
86 }
87
88 // return Webpack assets JSON object
89 // for serving it through HTTP service
90 return output
91}
92
93// populates the output object with assets
94function populate_assets(output, json, options, log)
95{
96 // for each chunk name ("main", "common", ...)
97 Object.keys(json.assetsByChunkName).forEach(function(name)
98 {
99 log.debug(`getting javascript and styles for chunk "${name}"`)
100
101 // get javascript chunk real file path
102
103 const javascript = get_assets(name, 'js')[0]
104 // the second asset is usually a source map
105
106 if (javascript)
107 {
108 log.debug(` (got javascript)`)
109 output.javascript[name] = javascript
110 }
111
112 // get style chunk real file path
113
114 const style = get_assets(name, 'css')[0]
115 // the second asset is usually a source map
116
117 if (style)
118 {
119 log.debug(` (got style)`)
120 output.styles[name] = style
121 }
122 })
123
124 // gets asset paths by name and extension of their chunk
125 function get_assets(name, extension = 'js')
126 {
127 let chunk = json.assetsByChunkName[name]
128
129 // a chunk could be a string or an array, so make sure it is an array
130 if (!(Array.isArray(chunk)))
131 {
132 chunk = [chunk]
133 }
134
135 return chunk
136 // filter by extension
137 .filter(name => path.extname(name) === `.${extension}`)
138 // adjust the real path (can be http, filesystem)
139 .map(name => options.assets_base_url + name)
140 }
141
142 // one can supply a custom filter
143 const default_filter = (module, regular_expression) => regular_expression.test(module.name)
144 // one can supply a custom namer
145 const default_asset_path = module => module.name
146 // one can supply a custom parser
147 const default_parser = module => module.source
148
149 // 1st pass
150 const parsed_assets = {}
151
152 // global paths to parsed asset paths
153 const global_paths_to_parsed_asset_paths = {}
154
155 // define __webpack_public_path__ webpack variable
156 // (resolves "ReferenceError: __webpack_public_path__ is not defined")
157 const define_webpack_public_path = 'var __webpack_public_path__ = ' + JSON.stringify(options.assets_base_url) + ';\n'
158
159 // for each user specified asset type
160 for (let asset_type of Object.keys(options.assets))
161 {
162 // asset type settings
163 const asset_type_settings = options.assets[asset_type]
164
165 // one can supply his own filter
166 const filter = (asset_type_settings.filter || default_filter) //.bind(this)
167 // one can supply his own path parser
168 const extract_asset_path = (asset_type_settings.path || default_asset_path) //.bind(this)
169 // one can supply his own parser
170 const parser = (asset_type_settings.parser || default_parser) //.bind(this)
171
172 // guard agains typos, etc
173
174 // for filter
175 if (!asset_type_settings.filter)
176 {
177 log.debug(`No filter specified for "${asset_type}" assets. Using a default one.`)
178 }
179
180 // for path parser
181 if (!asset_type_settings.path)
182 {
183 log.debug(`No path parser specified for "${asset_type}" assets. Using a default one.`)
184 }
185
186 // for parser
187 if (!asset_type_settings.parser)
188 {
189 log.debug(`No parser specified for "${asset_type}" assets. Using a default one.`)
190 }
191
192 log.debug(`parsing assets of type "${asset_type}"`)
193
194 // timer start
195 const began_at = new Date().getTime()
196
197 // get real paths for all the files from this asset type
198 json.modules
199 // take just modules of this asset type
200 .filter(module =>
201 {
202 // check that this asset is of the asset type
203 if (!filter(module, options.regular_expressions[asset_type], options, log))
204 {
205 return false
206 }
207
208 // guard against an empty source.
209 if (!module.source)
210 {
211 log.error(`Module "${module.name}" has no source. Maybe Webpack compilation of this module failed. Skipping this asset.`)
212 return false
213 }
214
215 // include this asset
216 return true
217 })
218 .reduce((set, module) =>
219 {
220 // determine asset real path
221 const asset_path = extract_asset_path(module, options, log)
222
223 // asset module source, or asset content (or whatever else)
224 const parsed_asset = parser(module, options, log)
225
226 log.trace(`Adding asset "${asset_path}", module id ${module.id} (in webpack-stats.json)`)
227
228 // check for naming collisions (just in case)
229 if (exists(set[asset_path]))
230 {
231 log.error('-----------------------------------------------------------------')
232 log.error(`Asset with path "${asset_path}" was overwritten because of path collision.`)
233 log.error(`Use the "filter" function of this asset type to narrow the results.`)
234 log.error(`Previous asset with this path:`)
235 log.error(set[asset_path])
236 log.error(`New asset with this path:`)
237 log.error(parsed_asset)
238 log.error('-----------------------------------------------------------------')
239 }
240
241 // add this asset to the list
242 //
243 // also resolve "ReferenceError: __webpack_public_path__ is not defined".
244 // because it may be a url-loaded resource (e.g. a font inside a style).
245 set[asset_path] = define_webpack_public_path + require_hacker.to_javascript_module_source(parsed_asset)
246
247 // add path mapping
248 global_paths_to_parsed_asset_paths[path.resolve(options.project_path, asset_path)] = asset_path
249
250 // continue
251 return set
252 },
253 parsed_assets)
254
255 // timer stop
256 log.debug(` time taken: ${new Date().getTime() - began_at} ms`)
257 }
258
259 // register a special require() hook for requiring() raw webpack modules
260 const require_hook = require_hacker.global_hook('webpack-module', (required_path, module) =>
261 {
262 log.debug(`require()ing "${required_path}"`)
263
264 // if Webpack aliases are supplied
265 if (options.alias)
266 {
267 // possibly alias the path
268 const aliased_global_path = alias_hook(required_path, module, options.project_path, options.alias, log)
269
270 // if an alias is found
271 if (aliased_global_path)
272 {
273 return require_hacker.to_javascript_module_source(safe_require(aliased_global_path, log))
274 }
275 }
276
277 // find an asset with this path
278 //
279 // the require()d path will be global path in case of the for..of require() loop
280 // for the assets (the code a couple of screens below).
281 //
282 // (it can be anything in other cases (e.g. nested require() calls from the assets))
283 //
284 if (exists(global_paths_to_parsed_asset_paths[required_path]))
285 {
286 log.debug(` found in parsed assets`)
287 return parsed_assets[global_paths_to_parsed_asset_paths[required_path]]
288 }
289
290 log.debug(` not found in parsed assets, searching in webpack stats`)
291
292 // find a webpack module which has a reason with this path
293
294 const candidates = []
295
296 for (let module of json.modules)
297 {
298 for (let reason of module.reasons)
299 {
300 if (reason.userRequest === required_path)
301 {
302 candidates.push(module)
303 break
304 }
305 }
306 }
307
308 // guard against ambiguity
309
310 if (candidates.length === 1)
311 {
312 log.debug(` found in webpack stats, module id ${candidates[0].id}`)
313
314 // also resolve "ReferenceError: __webpack_public_path__ is not defined".
315 // because it may be a url-loaded resource (e.g. a font inside a style).
316 return define_webpack_public_path + candidates[0].source
317 }
318
319 // if there are more than one candidate for this require()d path,
320 // then try to guess which one is the one require()d
321
322 if (candidates.length > 1)
323 {
324 log.debug(` More than a single candidate module was found in webpack stats for require()d path "${required_path}"`)
325
326 for (let candidate of candidates)
327 {
328 log.debug(' ', candidate)
329 }
330
331 // (loaders matter so the program can't simply throw them away from the required path)
332 //
333 // // tries to normalize a cryptic Webpack loader path
334 // // into a regular relative file path
335 // // https://webpack.github.io/docs/loaders.html
336 // let filesystem_required_path = last(required_path
337 // .replace(/^!!/, '')
338 // .replace(/^!/, '')
339 // .replace(/^-!/, '')
340 // .split('!'))
341
342 const fail = () =>
343 {
344 throw new Error(`More than a single candidate module was found in webpack stats for require()d path "${required_path}". Enable "debug: true" flag in webpack-isomorphic-tools configuration for more info.`)
345 }
346
347 // https://webpack.github.io/docs/loaders.html
348 const is_webpack_loader_path = required_path.indexOf('!') >= 0
349
350 // if it's a Webpack loader-powered path, the code gives up
351 if (is_webpack_loader_path)
352 {
353 fail()
354 }
355
356 // from here on it's either a filesystem path or an npm module path
357
358 const is_a_global_path = path => starts_with(path, '/') || path.indexOf(':') > 0
359 const is_a_relative_path = path => starts_with(path, './') || starts_with(path, '../')
360
361 const is_relative_path = is_a_relative_path(required_path)
362 const is_global_path = is_a_global_path(required_path)
363 const is_npm_module_path = !is_relative_path && !is_global_path
364
365 // if it's a global path it can be resolved right away
366 if (is_global_path)
367 {
368 return require_hacker.to_javascript_module_source(safe_require(required_path, log))
369 }
370
371 // from here on it's either a relative filesystem path or an npm module path,
372 // so it can be resolved against the require()ing file path (if it can be recovered).
373
374 // `module.filename` here can be anything, not just a filesystem absolute path,
375 // since some advanced require() hook trickery is involved.
376 // therefore it will be parsed.
377 //
378 let requiring_file_path = module.filename.replace(/\.webpack-module$/, '')
379
380 // if it's a webpack loader-powered path, then extract the filesystem path from it
381 if (requiring_file_path.indexOf('!') >= 0)
382 {
383 requiring_file_path = requiring_file_path.substring(requiring_file_path.lastIndexOf('!') + 1)
384 }
385
386 // make relative path global
387 if (is_a_relative_path(requiring_file_path))
388 {
389 requiring_file_path = path.resolve(options.project_path, requiring_file_path)
390 }
391
392 // if `requiring_file_path` is a filesystem path (not an npm module path),
393 // then the require()d path can possibly be resolved
394 if (is_a_global_path(requiring_file_path))
395 {
396 log.debug(` The module is being require()d from "${requiring_file_path}", so resolving the path against this file`)
397
398 // if it's a relative path, can try to resolve it
399 if (is_relative_path)
400 {
401 return require_hacker.to_javascript_module_source(safe_require(path.resolve(requiring_file_path, '..', required_path), log))
402 }
403
404 // if it's an npm module path (e.g. 'babel-runtime/core-js/object/assign'),
405 // can try to require() it from the requiring asset path
406 if (is_npm_module_path && is_a_global_path(module.filename))
407 {
408 return require_hacker.to_javascript_module_source(safe_require(require_hacker.resolve(required_path, module), log))
409 }
410 }
411
412 // if it's still here then it means it's either a
413 fail()
414 }
415 })
416
417 log.debug(`compiling assets`)
418
419 // timer start
420 const began_at = new Date().getTime()
421
422 // evaluate parsed assets source code
423 for (let asset_path of Object.keys(parsed_assets))
424 {
425 // set asset value
426 log.debug(`compiling asset "${asset_path}"`)
427 output.assets[asset_path] = safe_require(path.resolve(options.project_path, asset_path), log)
428
429 // inside that require() call above
430 // all the assets are resolved relative to this `module`,
431 // which is irrelevant because they are all absolute filesystem paths.
432 //
433 // if in some of those assets a nested require() call is present
434 // then it will be resolved relative to that asset folder.
435 }
436
437 // unmount the previously installed require() hook
438 require_hook.unmount()
439
440 // timer stop
441 log.debug(` time taken: ${new Date().getTime() - began_at} ms`)
442}
443
444function safe_require(path, log)
445{
446 try
447 {
448 return require(path)
449 }
450 catch (error)
451 {
452 log.error(error)
453 return undefined
454 }
455}
\No newline at end of file